iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0
Mobile Development

【Kotlin Notes And JetPack】Build an App系列 第 14

Day 14.【UI】ConstrainLayout 的介紹與應用

  • 分享至 

  • xImage
  •  

接下來就是針對介面拉出對應的元件位置,就像七巧板一樣,而 ConstrainLayout 可以幫助我們更容易調整元件的位置,以及更扁平化的方式作出複雜的結構,這篇會簡單的介紹 ConstrainLayout 有哪些功能可以使用,以下如有解釋不清或是描述錯誤的地方還請大家多多指教:

什麼?

就像開頭說的我們跟著 UI 設計出來的介面依樣畫葫蘆,將元件放在對應的位置上,ConstrainLayout 就像七巧板外框的進階版,讓不同形狀的版子更有彈性的擺放,那我們就來看看幾個功能吧!

| Position

1. 物件相對位置

針對畫面調整相對位置,如以下的按鈕要在文字的下面:
https://ithelp.ithome.com.tw/upload/images/20220927/20151145Ob3gI250nx.png
文字對按鈕往上 16dp,各自對邊邊 16dp,按鈕對下 50dp,我們可以寫成這樣:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools">

    <TextView
        android:id="@+id/isText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:text="我是文字" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/isButton"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="50dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:text="我是按鈕" />
</androidx.constraintlayout.widget.ConstraintLayout>
  • layout_constraintBottom_toTopOf 表示說我這個元件的底部要對上 XXid 的頂部,如果是對 constraintLayout 本身則是帶入 parent
  • layout_marginStart 表示說我這個元件的左邊要與連結的那邊間隔 XXdp,至於這邊也可以寫成 layout_marginLeft ,如果手機 app 是支援 rtl 語系區域,那就必須使用 start,而 right 必須寫成 end

2. Bias

物件可調整與另一個物件比例位置,使用 Bias 調整位置的前提之下是物件兩邊都需要 constraint,像是橫向調整則左右兩邊要有 constriant 的對像;直向則是上下要有對象,如以下文字想在 layout 左邊 20% 的位置:
https://ithelp.ithome.com.tw/upload/images/20220927/20151145hkzhvS7jhV.png

...
	<TextView
        android:id="@+id/isText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.2"
        app:layout_constraintStart_toStartOf="parent"
        tools:text="我是文字" />
...
  • layout_constraintHorizontal_bias 橫向的比例分配,由左到右從 0 到 1
  • layout_constraintVertical_bias 直向的比例,由上往下從 0 到 1
  • 如果有 margin 那就會是剩下的長度去分比例
  • 預設比例會是 0.5 / 0.5

3. Circle

以另一物件為中心設置半徑多少的距離,擺放想要的相對角度,如以下文字 半徑線 想在文字 圓心 半徑斜角 30 度的位置:
https://ithelp.ithome.com.tw/upload/images/20220927/20151145RQFpRT2lfc.png

...
	<TextView
        android:id="@+id/isRadius"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/isCenter"
        app:layout_constraintCircle="@id/isCenter"
        app:layout_constraintCircleAngle="30"
        app:layout_constraintCircleRadius="100dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="半徑線" />

    <TextView
        android:id="@+id/isCenter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="圓心" />
...
  • layout_constraintCircle 要以哪個 id 的元件為中心
  • layout_constraintCircleAngle 由上方為基準往幾度的位置
  • layout_constraintCircleRadiu 以圓心那個物件的中心點半徑多少 dp

4. Ratio

可以針對物件鎖定特定比例的長寬,我比較常使用到的情境是圖片的比例,如以下範例:
https://ithelp.ithome.com.tw/upload/images/20220927/20151145ZMMjXb1xF1.png

...
	<ImageView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:background="@color/black_1e"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/isButton"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="50dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:text="我是按鈕" />
...
  • layout_constraintDimensionRatio 圖片鎖定在 1:1 的大小,可以隨設計圖給的設定去調
  • 另外如果設計給的圖是 SVG 格式,一定要使用 app:srcCompat 去設定,因為低版本的手機不支援 vector drawable,而 srcCompat 有上下相容

| Visibility

可以設定物件是否顯示,在某些情境下元件不想一開始就是可見的狀態,這時候就可以透過需求來設定 Gone 或是 Invisible。
https://ithelp.ithome.com.tw/upload/images/20220927/20151145T3ViIbgy7C.png

1. Gone

會是整個物件消失,包含物件所佔的面積,所以有跟他有連接的對象就會往前推,如以下的狀況:

...
	<TextView
        android:id="@+id/isRadius"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="@+id/isCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/isCenter"
        app:layout_constraintTop_toTopOf="@+id/isCenter"
        tools:text="半徑線" />

    <TextView
        android:id="@+id/isCenter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
		android:visibility="gone"
        android:layout_marginStart="16dp"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="圓心" />
...

半徑線文字因為沒有特別設定又加上他的寬度是按照文字長度,所以預設位置會是中間,在與圓心文字沒有設定 margin 的情況下,將圓心設成 Gone 會變成以下樣子,整個字包含設定的 margin 16dp 都會不見。
https://ithelp.ithome.com.tw/upload/images/20220927/20151145WkZNBWcXZK.png

有時候我們會想要在某些物件顯示的時候 margin 跟物件隱藏時的 margin 不同,這時候可以在半徑線這個文字上加 layout_goneMarginStart="xxdp",就可以在 start 位置的物件 gone 的時候 margin xxdp

2. Invisible

物件會隱形但所佔面積不變,所以原本的位置並不會因此改變。

...
	<TextView
        android:id="@+id/isRadius"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="@+id/isCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/isCenter"
        app:layout_constraintTop_toTopOf="@+id/isCenter"
        tools:text="半徑線" />

    <TextView
        android:id="@+id/isCenter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:textSize="20sp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="圓心" />
...

圓心的字在設成 invisible 之後位置依然跟有顯示時一樣,半徑線也不會改變它原有的位置。
https://ithelp.ithome.com.tw/upload/images/20220927/20151145DYJJ9bNJ2u.png

如果想在 preview 的時候看到完整顯示的狀態可以加上 tools: visibility ="visible"

| Helper

以下介紹幾個我有使用過的 helper,用來處理一些特殊情況。

1. GuideLine

當你需要所有物件都向左偏移一樣的距離,又不想要每增加一個物件就要再寫一次,或是這次設計改了新的距離,所有物件都要改,但建議是有比較多重複性物件在使用,或是真的有需求性的設計,不然只要改動 guideline 依附在下面的物件也會更動。

Guideline 不是一個 view,所以我們只能在 preview 時看到,依樣有分成縱向跟橫向,有三種設定距離的方式:

  • layout_constraintGuide_begin
  • layout_constraintGuide_end
  • layout_constraintGuide_percent

https://ithelp.ithome.com.tw/upload/images/20220927/201511451gs8bKNCmM.png

...		
	<androidx.constraintlayout.widget.Guideline
        android:id="@+id/startGuideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="20dp" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/endGuideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_end="20dp" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/horizontalGuideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.4" />

    <TextView
        android:id="@+id/isRadius"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="@id/endGuideline"
        app:layout_constraintTop_toTopOf="@+id/horizontalGuideline"
        tools:text="半徑線" />

    <TextView
        android:id="@+id/isCenter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="@id/startGuideline"
        app:layout_constraintTop_toTopOf="@id/horizontalGuideline"
        tools:text="圓心" />
...

半徑線要以 endGuideline 為基準,所以要 end(right) constraint endGuideline;圓心 要以 startGuideline 為基準,所以 start(left) constraint startGuideline,percent 由上往下從 0 到 1。

2. Barrier

就像一個屏障一樣,物件會和對邊最長物件保持相同距離,這是什麼意思呢?來看範例吧:

今天設計出了一個任務卡的圖,未完成之前顯示分數的 TextView,跟完成後會顯示完成的圖,任務描述顯示的寬度間 5dp。
https://ithelp.ithome.com.tw/upload/images/20220927/20151145FNSZHsZnSi.png
與顯示分數的文字保持 5dp

https://ithelp.ithome.com.tw/upload/images/20220927/20151145tbSlXC5yFL.png
與完成的徽章也保持 5dp

因為文字和圖的寬度不一樣,狀態不一樣的情況下,其中一個物件會消失,所以文字的右邊不能 constriant 在任一個物件,不然會變成以下狀況:
https://ithelp.ithome.com.tw/upload/images/20220927/20151145HwhhHncKSV.png
constraint 的那方消失 margin 也不一樣

https://ithelp.ithome.com.tw/upload/images/20220927/201511455CMYavIDeb.png
只有 constraint 的那個物件顯示時會正常

...
	<androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="missionCollect, missionScore"
        app:barrierDirection="left" />

    <TextView
        android:id="@+id/missionTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="5dp"
        android:textSize="20sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="@+id/barrier"
        tools:text="今日任務:跑超場 15 圈 + 伏地挺身 100 下" />

    <ImageView
        android:id="@+id/missionCollect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        app:layout_constraintBottom_toBottomOf="@+id/missionTitle"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/missionTitle"
        app:srcCompat="@drawable/stamp" />

    <TextView
        android:id="@+id/missionScore"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:textSize="20sp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="@+id/missionTitle"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/missionTitle"
        tools:text="+ 100" />
...

跟 Guideline 一樣 Barrier 不是一個 view,所以只有在 preview 可以看到,維持的那邊要 constraint Barrier,顯示會改變的那方要加在 constraint_referenced_ids 參數中。

3. Group

當你有很多元件需要在某個情境下將 visibility 設成 Gone 或 Invisible,難道要一個一個元件寫嗎 ! 這時候我們就可以使用 Group,這樣只需要針對 Group 設定就行了,來看看以下範例:

...
    <TextView
        android:id="@+id/isRadius"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="@+id/isCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/isCenter"
        app:layout_constraintTop_toTopOf="@+id/isCenter"
        tools:text="半徑線" />

    <TextView
        android:id="@+id/isCenter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/isButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="圓心" />

    <androidx.constraintlayout.widget.Group
        android:id="@+id/group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="isCenter,isRadius" />
...

只要在 constraint_referenced_ids 寫上要一起群組的原件 id 就可以針對 Group 操作了,像是以下範例:

// binding 的部分後續篇章會介紹到
binding.group.isVisible = number == 2

如何?

接下來就要針對設計好的介面去排畫面,在上一篇建立 Fragment 時就有一起將 layout 的 XML 檔一起建立了,依序將畫面的元件排好,以主頁為範例:
https://ithelp.ithome.com.tw/upload/images/20220927/20151145BECOta2MFL.png

dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    ...
}
<androidx.constraintlayout.widget.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/dark_gray"
        tools:context=".HomeFragment">

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/cityList"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/arrowSign" />

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/cityTemList"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginBottom="24dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/addButton" />

    <ImageView
            android:id="@+id/arrowSign"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginBottom="20dp"
            android:src="@drawable/play"
            android:rotation="90"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/cityTemList" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/addButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="36dp"
            android:text="@string/app_name"
            app:backgroundTint="@color/white"
            app:fabSize="normal"
            app:srcCompat="@drawable/plus"
            app:tint="@null"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/settingButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="28dp"
            android:text="@string/app_name"
            app:backgroundTint="@color/white"
            app:fabSize="mini"
            app:srcCompat="@drawable/settings"
            app:tint="@null"
            app:layout_constraintBottom_toBottomOf="@+id/addButton"
            app:layout_constraintEnd_toStartOf="@+id/addButton"
            app:layout_constraintTop_toTopOf="@+id/addButton" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/searchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="28dp"
            android:text="@string/app_name"
            app:backgroundTint="@color/white"
            app:fabSize="mini"
            app:srcCompat="@drawable/magnifying_glass"
            app:tint="@null"
            app:layout_constraintBottom_toBottomOf="@+id/addButton"
            app:layout_constraintStart_toEndOf="@+id/addButton"
            app:layout_constraintTop_toTopOf="@+id/addButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

Reference

Android Developer


上一篇
Day 13.【UI】Fragment 的介紹與應用
下一篇
Day 15.【UI】Material Design Component 的介紹與應用
系列文
【Kotlin Notes And JetPack】Build an App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言